
package com.j256.ormlite.stmt;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.db.DatabaseType;
import com.j256.ormlite.field.FieldType;
import com.j256.ormlite.logger.Logger;
import com.j256.ormlite.logger.LoggerFactory;
import com.j256.ormlite.stmt.mapped.MappedPreparedStmt;
import com.j256.ormlite.table.TableInfo;

/**
 * Assists in building of SQL statements for a particular table in a particular
 * database.
 * 
 * @param T The class that the code will be operating on.
 * @param ID The class of the ID column associated with the class. The T class
 *            does not require an ID field. The class needs an ID parameter
 *            however so you can use Void or Object to satisfy the compiler.
 * @author graywatson
 */
public abstract class StatementBuilder<T, ID> {

    private static Logger logger = LoggerFactory.getLogger(StatementBuilder.class);

    protected final TableInfo<T, ID> tableInfo;
    protected final DatabaseType databaseType;
    protected final Dao<T, ID> dao;
    protected StatementType type;

    private Where<T, ID> where = null;

    // NOTE: anything added here should be added to the clear() method below

    public StatementBuilder(DatabaseType databaseType, TableInfo<T, ID> tableInfo, Dao<T, ID> dao,
            StatementType type) {
        this.databaseType = databaseType;
        this.tableInfo = tableInfo;
        this.dao = dao;
        this.type = type;
        if (!type.isOkForStatementBuilder()) {
            throw new IllegalStateException("Building a statement from a " + type
                    + " statement is not allowed");
        }
    }

    /**
     * Returns a {@link Where} object that should be used to add SQL where
     * clauses to the statement. This will also reset the where object so you
     * can use the same query builder with a different where statement.
     */
    public Where<T, ID> where() {
        where = new Where<T, ID>(tableInfo, this, databaseType);
        return where;
    }

    /**
     * Set the {@link Where} object on the query. This allows someone to use the
     * same Where object on multiple queries.
     */
    public void setWhere(Where<T, ID> where) {
        this.where = where;
    }

    /**
     * Prepare our statement for the subclasses.
     * 
     * @param limit Limit for queries. Can be null if none.
     */
    protected MappedPreparedStmt<T, ID> prepareStatement(Long limit) throws SQLException {
        List<ArgumentHolder> argList = new ArrayList<ArgumentHolder>();
        String statement = buildStatementString(argList);
        ArgumentHolder[] selectArgs = argList.toArray(new ArgumentHolder[argList.size()]);
        FieldType[] resultFieldTypes = getResultFieldTypes();
        FieldType[] argFieldTypes = new FieldType[argList.size()];
        ;
        for (int selectC = 0; selectC < selectArgs.length; selectC++) {
            argFieldTypes[selectC] = selectArgs[selectC].getFieldType();
        }
        if (!type.isOkForStatementBuilder()) {
            throw new IllegalStateException("Building a statement from a " + type
                    + " statement is not allowed");
        }
        return new MappedPreparedStmt<T, ID>(tableInfo, statement, argFieldTypes, resultFieldTypes,
                selectArgs, (databaseType.isLimitSqlSupported() ? null : limit), type);
    }

    /**
     * Build and return a string version of the query. If you change the where
     * or make other calls you will need to re-call this method to re-prepare
     * the query for execution.
     */
    public String prepareStatementString() throws SQLException {
        List<ArgumentHolder> argList = new ArrayList<ArgumentHolder>();
        return buildStatementString(argList);
    }

    /**
     * Clear out all of the statement settings so we can reuse the builder.
     */
    public void clear() {
        where = null;
    }

    private String buildStatementString(List<ArgumentHolder> argList) throws SQLException {
        StringBuilder sb = new StringBuilder(128);
        appendStatementString(sb, argList);
        String statement = sb.toString();
        logger.debug("built statement {}", statement);
        return statement;
    }

    /**
     * Internal method to build a query while tracking various arguments. Users
     * should use the {@link #prepareStatementString()} method instead.
     * <p>
     * This needs to be protected because of (WARNING: DO NOT MAKE A JAVADOC
     * LINK) InternalQueryBuilder (WARNING: DO NOT MAKE A JAVADOC LINK).
     * </p>
     */
    protected void appendStatementString(StringBuilder sb, List<ArgumentHolder> argList)
            throws SQLException {
        appendStatementStart(sb, argList);
        if (where != null) {
            sb.append("WHERE ");
            where.appendSql(sb, argList);
        }
        appendStatementEnd(sb);
    }

    /**
     * Append the start of our statement string to the StringBuilder.
     */
    protected abstract void appendStatementStart(StringBuilder sb, List<ArgumentHolder> argList)
            throws SQLException;

    /**
     * Get the result array from our statement after the
     * {@link #appendStatementStart(StringBuilder, List)} was called. This will
     * be null except for the QueryBuilder.
     */
    protected FieldType[] getResultFieldTypes() {
        return null;
    }

    /**
     * Append the end of our statement string to the StringBuilder.
     */
    protected abstract void appendStatementEnd(StringBuilder sb) throws SQLException;

    /**
     * Verify the columnName is valid and return its FieldType.
     * 
     * @throws IllegalArgumentException if the column name is not valid.
     */
    protected FieldType verifyColumnName(String columnName) {
        return tableInfo.getFieldTypeByColumnName(columnName);
    }

    /**
     * Return the type of the statement.
     */
    StatementType getType() {
        return type;
    }

    /**
     * Types of statements that we are building.
     */
    public static enum StatementType {
        /** SQL statement in the form of SELECT ... */
        SELECT(true, true, false, false),
        /** SQL statement in the form of SELECT COUNT(*)... or something */
        SELECT_LONG(true, true, false, false),
        /**
         * SQL statement in the form of SELECT... with aggregate functions or
         * something
         */
        SELECT_RAW(true, true, false, false),
        /** SQL statement in the form of UPDATE ... */
        UPDATE(true, false, true, false),
        /** SQL statement in the form of DELETE ... */
        DELETE(true, false, true, false),
        /**
         * SQL statement in the form of CREATE TABLE, ALTER TABLE, or something
         * returning the number of rows affected
         */
        EXECUTE(false, false, false, true),
        // end
        ;

        private final boolean okForStatementBuilder;
        private final boolean okForQuery;
        private final boolean okForUpdate;
        private final boolean okForExecute;

        private StatementType(boolean okForStatementBuilder, boolean okForQuery,
                boolean okForUpdate, boolean okForExecute) {
            this.okForStatementBuilder = okForStatementBuilder;
            this.okForQuery = okForQuery;
            this.okForUpdate = okForUpdate;
            this.okForExecute = okForExecute;
        }

        public boolean isOkForStatementBuilder() {
            return okForStatementBuilder;
        }

        public boolean isOkForQuery() {
            return okForQuery;
        }

        public boolean isOkForUpdate() {
            return okForUpdate;
        }

        public boolean isOkForExecute() {
            return okForExecute;
        }
    }
}
